using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Web.WebPages;
using Eksponent.RazorRocks.Helpers;

namespace Eksponent.RazorRocks
{
    public interface ITemplateMatcher
    {
        HelperResult Render(object o);
    }


    public partial class TemplateMatcher<TItem> : ITemplateMatcher where TItem : class
    {
        private readonly TypeInfo _defaultApriori = "__@INTERNAL >*", // Yeah, don't use these names for your types...
                                         _defaultApostiori = "__@INTERNAL *>",
                                         _nullKey = "__@INTERNAL NULL";

        private const string NestingDepthKey = "TemplateMatcher.Nesting.Depth";

        private readonly Func<TItem, TypeInfo> _typeNameResolver;

        private int _maxNestingDepth = 30;

        private readonly Dictionary<MultiKey, List<TemplateController<TItem>>> _templates =
            new Dictionary<MultiKey, List<TemplateController<TItem>>>();

        private string _basePath;

        /// <summary>
        /// If no direct match is found for a type's name the base types are sorted according to these priorities
        /// </summary>
        private Dictionary<MultiKey, double> _typePriorities = new Dictionary<MultiKey, double>();


        public TemplateMatcher(Func<TItem, TypeInfo> typeNameResolver, string basePath = "~/macroScripts/RazorRocks/") 
        {
            _typeNameResolver = typeNameResolver;
            Configure().Default.Match("Default").SetBasePath(basePath);
        }

        public IEnumerable<TemplateSpecification<TItem>> GetTemplatesFor(TItem item, IDictionary<string, object> parameters = null, params string[] subfolders)
        {
            var templates = new List<TemplateSpecification<TItem>>();

            subfolders = subfolders.IsNullOrEmpty() ? new[] { "" } : subfolders;

            foreach (string subfolder in subfolders)
            {
                AddTemplatesFor(item, subfolder, templates, parameters, targetSubfolder: subfolder, typeInfo: _defaultApriori);

                if (!AddTemplatesFor(item, subfolder, templates, parameters, targetSubfolder: subfolder))
                {
                    var thisSubFolder = subfolder;
                    templates.AddRange(
                        _typeNameResolver(item).AllNames.Select(x =>
                                                                new TemplateSpecification<TItem>() { Action = x, Subfolder = thisSubFolder }));
                }

                AddTemplatesFor(item, subfolder, templates, parameters, targetSubfolder: subfolder, typeInfo: _defaultApostiori);
            }

            return templates.Distinct();
        }

        #region Rendering
        //TODO: These doesn't (necessarily) belong here. They are specific to RazorTemplateAction

        internal static readonly Dictionary<string, bool> FileCache = new Dictionary<string, bool>();
        internal static readonly Dictionary<string, Type> PreProcessors = new Dictionary<string, Type>();
        static volatile bool PreProcessorsLoaded = false;

        private FileSystemWatcher _templateWatcher;

        public static void ClearTemplateCache()
        {
            FileCache.Clear();
        }

        public HelperResult Render(object o)
        {
            return Render((TItem)o, null);
        }


        public TemplateHelperResult Render(TItem item, IDictionary<string, object> parameters, params string[] subFolders)
        {
            var templates = GetTemplatesFor(item, parameters, (subFolders ?? new string[0]).ToArray());
            foreach (var template in templates)
            {
                var result = RenderInternal(item, template.Action, parameters, template.Subfolder);
                if (result != null)
                {
                    return result;
                }
            }

            throw new NotSupportedException(
                string.Format(
                    "No template was found for {0} (searched {1})",
                    _typeNameResolver(item).Name,
                    string.Join(", ",
                                templates.Select(
                                    x =>
                                    x.Action + (!string.IsNullOrEmpty(x.Subfolder) ? "(" + x.Subfolder + ")" : "")))));
        }
                      
        /// <summary>
        /// Renders the item with the specified action.
        /// </summary>
        /// <param name="item">The item.</param>
        /// <param name="action">The action.</param>
        /// <param name="args">The args.</param>
        /// <returns></returns>
        public TemplateHelperResult RenderWith(TItem item, TemplateAction<TItem> action, IDictionary<string, object> args = null)
        {
            return RenderInternal(item, action, args);
        }
        

        private TemplateHelperResult RenderInternal(TItem item, TemplateAction<TItem> action, IDictionary<string, object> parameters, string subfolder = "")
        {               
                     
            var ctx = new TemplateRenderContext<TItem>
            {
                BasePath = _basePath,
                FileCache = FileCache,                
                Parameters = parameters,
                Subfolder = subfolder,
                Item = item
            };

            var result = action.Render(item, ctx);
            if (result != null)
            {
                //A little dirty... Wrap the TemplateHelperResult in a new one that decrements scope
                var wrappedResult = new TemplateHelperResult(w =>
                                                                 {
                                                                     OpenScope(ctx);
                                                                     result.Action(w);
                                                                     CloseScope();
                                                                 }, result.ReturnValues);
                return wrappedResult;
            }            

            return null;
        }

        void OpenScope(TemplateRenderContext<TItem> ctx)
        {
            ContextStack.Push(ctx);
            
            if (ContextStack.Count > _maxNestingDepth)
            {
                throw new InvalidOperationException("Nesting depth exceeded (after " + ContextStack.Count + " levels of recursion). Have you forgot to specify a subfolder somewhere?");
            }            
        }

        void CloseScope()
        {
            ContextStack.Pop();                       
        }


        private bool AddTemplatesFor(TItem item, string subfolder, List<TemplateSpecification<TItem>> targetTemplates, IDictionary<string, object> parameters, string targetSubfolder = null, TypeInfo typeInfo = null)
        {
            typeInfo = typeInfo ?? _typeNameResolver(item);

            bool any = false;

            var typeNames = new List<string>();
            typeNames.Add(typeInfo.Name);
            //Sort base names according to priorities
            if (typeInfo.BaseNames != null)
            {
                typeNames.AddRange(typeInfo.BaseNames.OrderBy(baseName =>
                    _typePriorities.GetOrDefault(new MultiKey(typeInfo.Name, baseName), () =>
                        _typePriorities.GetOrDefault(new MultiKey(baseName), () => 1)
                    )));
            }

            foreach (var typeName in typeInfo.AllNames)
            {
                MultiKey key = string.IsNullOrEmpty(subfolder)
                                   ? new MultiKey(typeName)
                                   : new MultiKey(typeName, subfolder);
                var templates = _templates.GetOrDefault(key);

                if (templates != null)
                {
                    var specs = templates.Select(
                        x => new TemplateSpecification<TItem> { Action = x.GetAction(item, parameters), Subfolder = targetSubfolder ?? subfolder })
                        .Where(x => x.Action != null).ToList();
                    targetTemplates.AddRange(specs);
                    any = true;
                }
            }

            if (!any && !string.IsNullOrEmpty(subfolder))
            {
                return AddTemplatesFor(item, null, targetTemplates, parameters, targetSubfolder, typeInfo); //... try adding templates from defaults without specific subfolder
            }

            return any;
        }

        #endregion

        #region Parameters

        private const string ContextStackKeyPrefix = "TemplateMatcher.ContextStack";

        private static Stack<TemplateRenderContext<TItem>> ContextStack
        {
            get { return RequestScoped.Get(ContextStackKeyPrefix + typeof(TItem).Name, () => new Stack<TemplateRenderContext<TItem>>()); }
        }

        public TemplateRenderContext<TItem> CurrentContext
        {
            get { return ContextStack.Peek(); }
        }

        #endregion
    }
}